/*
 * @description: 描述
 * @Author: 问道师兄木木木
 * @Date: 2021-02-05 14:02:13
 * @LastEditors: 问道师兄木木木
 * @LastEditTime: 2021-02-15 14:25:11
 */

//todo 类
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return `Hello, ${this.greeting}`;
    }
}

let greeterClass = new Greeter("world");
console.log(greeterClass.greet());


//todo 继承

//? 在TypeScript里，我们可以使用常用的面向对象模式。基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。

class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}`);
    }
}

class Dog extends Animal {
    bark() { 
        console.log('Woof! Woof!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

//? 这个例子展示了最基本的继承：类从基类中继承了属性和方法。这里，Dog是一个 派生类，它派生自 Animal 基类
//? 通过 extends关键字。派生类通常被称作 子类，基类通常被称作 超类。
//*
//? 因为 Dog继承了 Animal 的功能，因此我们可以创建一个 Dog的实例，它能够 bark() 和 move()。 

class Animal1 {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
    newDate(){
        return new Date().getMinutes();
    }
}

class Snake extends Animal1 {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
        console.log(`snake: ${super.newDate()}m`);
    }
}

class Horse extends Animal1 {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    } 
}

let sam = new Snake("Sammy the Python");
let tom: Animal1 = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

//? 与之前一个例子的不同点是，派生类（子类）包含了一个构造函数，它 必须调用 super(),它会执行基类（超类）的构造函数。
//? 而且，在构造函数里访问 this的属性之前，我们 一定要调用 super()。 这个是TypeScript强制执行的一条重要规则。

//? 上面这个例子演示了如何在子类里可以重写父类的方法。Snake 类和 Horse 类 都创建了 move 方法，
//? 它们重写了从 Animal1 继承来的 move方法，使得 move方法根据不同的类而具有不同的功能。
//? 注意，即使 tom被声明为 Animal1 类型，但因为它的值是 Horse，调用 tom.move(34) 时，它会调用 Horse 里重写的方法：

//* Slithering...
//* Sammy the Python moved 5m.
//* Galloping...
//* Tommy the Palomino moved 34m.



//todo 公共，私有与受保护的修饰符
//! 默认为 public

//? 在上面的例子里，我们可以自由的访问程序里定义的成员。如果你对其它语言中的类比较了解，就会注意到我们在之前的代码
//? 里并没有使用 public 来做修饰符; 例如，C#要求必须明确地使用 public 指定成员是可见的。在TypeScript里，成员都默认为public.

//? 你也可以明确的将一个成员标记成 public。我们可以用下面的方式来重写上面的 Animal1 类：
class Animal2 {
    public name: string;
    public constructor(theName: string) { this.name = theName; } 
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`)
    }
}

//! 理解 private
//? 当成员被标记成 private 时，它就不能在声明它的类的外部访问。比如：
class Animal3 {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

//* new Animal3("Cat").name; /** 错误：'name'是私有的 */

//? TypeScript使用的是 结构性类型系统。当我们比较两种不同的类型时，并不在乎它们从何处而来，如果所有成员的类型都是兼容
//? 的，我们就认为它们的类型是兼容的。

//? 然而，当我们比较带有 private 或 protected 成员的类型的时候，情况就不同了。如果其中一个类型里包含一个 private成员,
//? 那么只有另外一个类型中存在这样一个 private 成员，并且它们都是来自同一处声明时，我们才认为这两个类型是兼容的。
//? 对于 protected 成员也使用这个规则。

//? 下面来看一个例子，更好地说明了这一点：
class Animal4 {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

class Rhino extends Animal4 {
    constructor() { super("Rhino"); }
}

class Employee {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

let animal = new Animal4("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

animal = rhino;
// * animal = employee;  /** 错误：Animal4 与 Employee 不兼容 */

//? 这个例子中有 Animal4和 Rhino 两个类，Rhino 是 Animal4的子类。还有一个 Employee 类，其类型看上去与 Animal4是相同的。
//? 我们创建了几个这些类的实例，并相互赋值来看看会发生什么。因为 Animal4 和 Rhino 共享了来自 Animal4里的私有成员定义 private name: string
//? 因此它们是兼容的。然而 Employee却不是这样。当把 Employee 赋值给 Animal4的时候，得到一个错误，说它们的类型不兼容。
//? 尽管 Employee里也有一个私有成员 name,但它明显不是 Animal4里面定义的那个。

//! 理解 protected
//? protected 修饰符与 private 修饰符的行为很相似，但有一点不同， protected 成员在派生类（子类）中仍然可以访问。
//? 例如：
class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee1 extends Person {
    private department: string;

    constructor(name: string, department:string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee1("Howard","Sales");
console.log(howard.getElevatorPitch());
//* console.log(howard.name);  /** 错误 */

//? 注意，我们不能在 Person类外使用 name, 但是我们仍然可以通过 Employee 类的实例方法访问，因为 Employee是由 Person派生而来的。

//? 构造函数也可以被标记成 protected。这意味着这个类不能在包含它的类外被实例化，但是能被继承。比如：
class Person1 {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}

// Employee 能够继承 Person
class Employee2 extends Person1 {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard1 = new Employee2("Howard", "Sales");
//* let john = new Person1("John"); /** 错误：'Person' 的构造函数是被保护的。 */

//! readonly修饰符
//? 你可以使用 readonly 关键字将属性设置为只读的。只读属性必须在声明时或构造函数里被初始化。

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 0;
    constructor (theName: string) {
        this.name = theName;
    }
}

let dad = new Octopus("Man with the 8 strong legs");
//* dad.name = "Man width the 3-piece suit";  /** 错误！name 是只读的 */

//* 参数属性
//? 在上面的例子中，我们必须在 Octopus 类里定义一个只读成员 name和一个参数为 theName的构造函数
//? 并且立刻将 theName的值赋值给 name,这种情况经常会遇到。 参数属性可以方便让我们在一个地方定义并初始化一个成员
//? 下面的例子是对之前 Octopus 类的修改版，使用了参数属性：
class Octopus1 {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) { }
}

//? 注意看我们是如何舍弃了 theName, 仅在构造函数里使用 readonly name: string 参数来创建和初始化 name成员
//? 我们把声明和赋值合并至一处。

//? 参数属性通过给构造函数参数前面添加一个访问限定符来声明。使用 private 限定一个参数属性会声明并初始化一个私有成员；
//? 对于 public 和 protected 来说也是一样。

//* 存取器
//? TypeScript 支持通过 getter/setter 来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
//? 下面来看如何把一个简单的类改写成使用 get和 set。 首先，我们从一个没有使用存取器的例子开始。
class Employee3 {
    fullName: string = undefined;
} 

let employee3 = new Employee3();
employee3.fullName = "Bob Smith";
if(employee3.fullName) console.log(employee3.fullName)

//? 我们可以随意的设置 fullName , 这是非常方便的，但是这也可能会带来麻烦。

//? 下面这个版本里，我们先检查用户密码是否正确，然后在允许其修改员工信息。我们把对 fullName 的直接访问改
//? 成了可以检查密码的 set 方法。我们也加入了一个 get 方法，让上面的例子仍然可以工作。

class Employee4 {

    passcode: string = undefined;

    private _fullName: string = 'mu';

    get fullName(): string { 
        return this._fullName;
    }

    set fullName(newName: string) {
        if(this.passcode && this.passcode == "secret passcode") this._fullName = newName;
        else console.log("Error: Unauthorized update of employee!"); // 未经授权的员工更新
    }
}

//? 不是 ts 的内容，node 的命令行接受输入知识，还有引入node
const readline = require('readline').createInterface({
    input: process.stdin,
    output: process.stdout
})

readline.question(`请输入密码:`, (password: string) => {
    let employee4 = new Employee4()
    employee4.passcode = password
    employee4.fullName = "Bob Smith1"
    if(employee4.fullName) console.log(employee4)

    readline.close()
})

//? 我们可以修改一下密码，来验证一下存取器是否是工作的。 当密码不对时，会提示我们没有权限去修改员工。

//? 对于存取器有下面几点需要注意的：
//? 首先，存取器要求你将编译器设置为输出 ECMAScript 5或更高。 不支持降级别到ECMAScript 3。其次，只带有 get
//? 不带有 set 的存取器自动被推断为 readonly。在这从代码生成 class.ts 文件是有帮助的，因为利用这个属性的
//? 用户会看到不允许改变它的值。


//* 静态属性

//? 到目前为止，我们只讨论了类的实例成员，那些仅当类被实例化的时候才会被初始化的树。我们也可以创建类的静态成员，
//? 这些属性存在于类本身上面而不是类的实例上面。这个例子里，我们使用 static 定义 origin，因为它是所有网格都会
//? 用到的属性。每个实例想要访问这个属性的时候，都要在 origin 前面加上类名。如同在实例属性上使用 this. 前缀
//? 来访问属性一样，这里我们使用 Grid. 来访问静态属性

class Grid {

    static origin = {x: 0, y: 0};

    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }

    constructor (public scale: number) { }
}

let grid1 = new Grid(1.0);
let grid2 = new Grid(5.0);

console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

//* 抽象类

//? 抽象类作为其他派生类（子类）的基类（超类 父类？）使用。它们一般不会直接被实例化。不同于接口，抽象类可以包含成员的实现细节。
//? abstract 关键字是用于定义抽象类和在抽象类内部定义的抽象方法。

abstract class Animal5 {

    abstract makeSound(): void;

    move(): void {
        console.log('roaming hte earch...');
    }
}

//? 抽象类中的抽象方法不包含具体实现并且必须在派生类（子类）中实现。抽象方法的语法与接口方法像是。两者都是定义方法签名
//? 但不包含方法体。然而，抽象方法必须包含 abstract 关键字并且可以包含访问修饰符。

abstract class Department {
    constructor(public name: string){
    }

    printName(): void {
        console.log(`Department name: ${this.name}`);
    }

    abstract printMeeting(): void;   /** 必须在派生类(子类)中实现 */
}

class AccountingDepartment extends Department{
    
    constructor() {
        super('Accounting and Auditing');
    }

    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }

    generateReports(): void {
        console.log('Generating accounting reports...')
    }
}

let department: Department;  /** 允许创建一个对抽象类型的引用 */
// department = new Department(); /** 错误：不能创建一个抽象类的实例 */
department = new AccountingDepartment(); /** 允许对一个抽象类类进行实例化和赋值 */
department.printName();
department.printMeeting();
// department.generateReports(); /** 错误：方法在声明的抽象类中不存在 */


//! 高级技巧

//* 构造函数
//? 当你在 TypeScript 里声明了一个类的时候，实际上同时声明了很多东西。首先就是类的 实例的类型

class Greeter1 {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    greet() {
        return `Hello, ${this.greeting}`
    }
}

let greeter1: Greeter1;
greeter1 = new Greeter1("world");
console.log(greeter1.greet());

//? 这里，我们写了 let greeter: Greeter , 意思是 Greeter 类的实例的类型是 Greeter 。这对于用过其他面向对象语言的程序员
//? 来讲已经是老习惯了。

//? 我们也创建了一个叫做 构造函数的值。这个函数会在我们使用 new 创建实例的时候被调用。下面我们来看看，
//? 上面的代码被编译成 JavaScript 后是什么样子的：

/**
 * let Greeter = (function () {
 *      function Greeter(message) {
 *          this.greeting = message;
 *      }
 *         
 *      Greeter.prototype.greet = function () {
 *          return "Hello, " + this.greeting;
 *      };
 *      return Greeter;
 * })();
 * 
 * let greeter;
 * greeter = new Greeter("world");
 * console.log(greeter.greet());
 */

 //? 上面的代码里，let Greeter 将被赋值为构造函数。当我们调用 new 并执行了这个函数后，便会得到一个类的实例
 //? 这个构造函数也包含了类的所有静态属性。换个角度说，我们可以认为类具有 实例部分与 静态部分这两个部分

 //? 让我们稍微改写一下这个例子，看看它们的区别：
 
 class Greeter2 {
     static standardGreeting = "Hello, there"
     greeting: string
     
     greet() {
         if(this.greeting) return `Hello, ${this.greeting}`
         else return Greeter2.standardGreeting
     }
 }

 let greeter2: Greeter2
 greeter2 = new Greeter2()
 console.log(greeter2.greet())

let greeterMaker: typeof Greeter2 = Greeter2   /** 这部分堪称 let greeterMaker: typeof Greeter2 是一部分， 赋值 Greeter2() */
greeterMaker.standardGreeting = "Hey there!"

let greeter3: Greeter2 = new greeterMaker()
console.log(greeter3.greet())

//? 这个例子里，greeter2 与之前看到的一样。我们实例化 Greeter2 类，并使用这个对象。我们之前看到的一样。

//? 再之后，我们直接使用类。我们创建了一个叫做 greeterMaker 的变量。这个变量保存了这个类或者说保存了类 构造函数
//? 然后我们使用 typeof Greeter2 ，意思是取Greeter2类的类型（function） ,而不是实例的类型。或者更确切的说，
//? "告诉我 Greeter2 标识符的类型"，也就是构造函数的类型。这个类型包含了类的所有静态成员和构造函数。之后，就和前面一样，
//? 我们在 greeterMaker 上使用 new， 创建 Greeter 的实例。



//* 把类当做接口使用

//? 如上一节里所讲的，类定义会创建两个东西： 类的实例类型和一个构造函数。因为类可以创建出类型，所以你能够在
//? 允许使用接口的地方使用类

class Point {
    x: number
    y: number
}

interface Point3d extends Point {
    z: number
}

let point3d: Point3d = {x: 1, y: 2, z: 3};
console.log(point3d);